import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosResponseHeaders, CancelTokenSource, RawAxiosResponseHeaders } from 'axios'
import dayjs from 'dayjs'
import { isNil, isString } from 'lodash-es'
import { stringify } from 'qs'
import { CacheOptions, getCache, setCache } from '../cache'
import { getAppUrl } from '../config'
import { appConfig } from '../config/app';
import { BaseType, Data, emitter, HTTP_PREFIX, HTTPS_PREFIX, IAppKey, login, newError, PUSH, uuid } from '../index';
import { serverTime } from '../server'
import { asyncDownloadFile, listToTree, putChild, removeNullValues } from '../util'
import { HttpConfig, httpConfig } from './config'


export * from './config'

export interface HttpCache {
  expire?: number //秒
  key?: string
  options?: CacheOptions
}

export interface HttpRequestConfig extends AxiosRequestConfig, IAppKey {
  cache?: HttpCache
  cancelTokenName?: string
  encrypt?: boolean
  ignoreError?: boolean
  ignoreToken?: boolean
  refresh?: boolean
  tree?: boolean
}

export enum HttpMethod {
  GET,
  POST,
  DEL,
  PUT,
}

export class HttpError extends Error {
  msg: string

  constructor(message: string) {
    super(message)
    this.msg = message
  }
}

export class HttpResponseError extends HttpError {
  code?: number
  data?: any
  response?: AxiosResponse
  config?: HttpRequestConfig

  constructor(message: string) {
    super(message)
  }
}

export const CONTENT_TYPE = 'content-type'
export const APPLICATION_JSON = 'application/json'
export const MULTIPART_FORM_DATA = 'multipart/form-data'
export const APPLICATION_FORM_URLENCODED = 'application/x-www-form-urlencoded'
export const CONTENT_DISPOSITION = 'content-disposition'

export const ErrorMessage: Data = {
  contentDisposition: '返回的http头content-disposition中没有附件名称',
  contentType: '返回的数据非标准JSON格式',
  network: '网络错误',
  other: '系统错误',
  timeout: '接口调用超时',
}

export const CodeMessage: Data = {
  404: '404：找不到接口',
  405: '405：调用方法错误',
  406: '406：请求的格式不可得',
  415: '415：不支持的媒体类型',
  502: '502：网关错误',
  503: '503：服务不可用',
  504: '504：网关超时',
}

const file = (contentType: string, response: AxiosResponse): void => {
  const { config, headers, data } = response
  if (contentType === APPLICATION_JSON) {
    const read = new FileReader()
    read.readAsText(data)
    read.onload = (): any | Promise<any> => {
      try {
        const result: any = read.result
        let json = {}
        try {
          json = JSON.parse(result)
        } catch (e) {
          console.error(e)
        }
        return afterResponse(json, config, response)
      } catch (e) {
        if (e instanceof HttpResponseError) {
          e.response = response
          e.config = config
          return throwError(e)
        } else {
          console.error(e)
          throw new HttpError('未知异常')
        }
      }
    }
  } else {
    let filename = ''
    const disposition = headers[CONTENT_DISPOSITION]
    console.info(headers)
    if (disposition) {
      try {
        filename = decodeURI(disposition.split('=')[1])
        console.info(filename)
      } catch {
        console.error(ErrorMessage.contentDisposition)
      }
    }
    asyncDownloadFile(data, filename)
  }
}

export const isFile = (data: Blob | File): boolean => data.constructor === Blob || data.constructor === File

const contentTypeFilter = (headers: AxiosResponseHeaders | RawAxiosResponseHeaders): string => {
  const ct = headers[CONTENT_TYPE]
  if (ct) {
    return (ct.toLowerCase().split(';')[0]).trim()
  } else {
    return APPLICATION_JSON
  }
}

const throwError = async (e: HttpResponseError): Promise<any> => {
  if (e.message !== 'CancelToken') {
    const result = await httpConfig.beforeThrowError(e)
    if (result) {
      return result
    }
  }
  throw e
}

const setBaseURL = (config: HttpRequestConfig): void => {
  config.baseURL = getAppUrl({ app: config.appKey })
}

const setTimeout = (config: HttpRequestConfig): void => {
  if (!config.timeout) {
    if (config.responseType === 'blob') {
      config.timeout = 5 * 60 * 1000
    }
  }
}

const doListToTree = (isTree: boolean, result: any) => {
  if (isTree && result) {
    return listToTree(result)
  } else {
    return result
  }
}

interface SourceTokenType {
  source: CancelTokenSource | null
  isPending: boolean
}

const responseErr = (error: any): any | Promise<any> => {
  const { config, response, message } = error

  if (message?.includes('timeout')) {
    error.message = ErrorMessage.timeout
  } else if (message?.includes('Network Error')) {
    error.message = ErrorMessage.network
  }

  if (response) {
    if (response.status !== 0) {
      const { status } = response
      if (CodeMessage[status]) {
        if (appConfig.env === 'dev') {
          error.message = `状态码：${status}，异常信息：${response.data.message || error.message}`
        } else {
          error.message = CodeMessage[status] || response.statusText
        }
      } else {
        if (contentTypeFilter(response.headers) === APPLICATION_JSON) {
          try {
            return afterResponse(response.data, config, response)
          } catch (e) {
            if (e instanceof HttpResponseError) {
              e.response = response
              e.config = config
              return throwError(e)
            } else {
              console.error(e)
              throw new Error('未知异常')
            }
          }
        } else {
          error.message = ErrorMessage.contentType
        }
      }
    }
  } else {
    error.message = ErrorMessage.network
  }
  const e = new HttpResponseError(error.message)
  e.response = response
  e.config = config
  return throwError(e)
}

const responseOk = (response: AxiosResponse) => {
  const { config, headers, data } = response
  const contentType = contentTypeFilter(headers)
  if (isFile(data)) {
    return file(contentType, response)
  } else {
    if (contentType === APPLICATION_JSON) {
      try {
        return afterResponse(data, config, response)
      } catch (e) {
        if (e instanceof HttpResponseError) {
          e.response = response
          e.config = config
          return throwError(e)
        } else {
          throw new HttpError('未知异常')
        }
      }
    } else {
      const e = new HttpResponseError(ErrorMessage.contentType)
      e.response = response
      e.config = config
      return throwError(e)
    }
  }
}

const moveCancelToken = (params: any, config?: HttpRequestConfig) => {
  if (params && params.cancelTokenName) {
    if (config) {
      config.cancelTokenName = params.cancelTokenName
    }
    delete params.cancelTokenName
  }
  return config
}

const afterResponse = (json: any, config: HttpRequestConfig, response: AxiosResponse) => {
  const date = response.headers.date
  if (date) {
    serverTime.value = dayjs(date).valueOf()
    httpConfig.checkDate(serverTime.value)
  }
  const push = response.headers[appConfig.app.pushHeaderName]
  if (push) {
    emitter.emit(PUSH, { value: push, date: serverTime.value })
  }
  return httpConfig.afterResponse(json, config, response)
}

export default class Http {
  public static ajax: AxiosInstance
  private static sourceMap = new Map()
  private questionMark = {
    addQueryPrefix: true,
    allowDots: true,
    indices: false,
    skipNulls: true,
  }

  constructor(config?: HttpConfig) {
    if (config) {
      for (const key in config) {
        // @ts-ignore
        httpConfig[key] = config[key]
      }
    }
  }

  private static async request0<R>(method: HttpMethod, url: string, params: any, config: HttpRequestConfig): Promise<AxiosResponse<R>> {
    if (isString(config.cancelTokenName)) {
      const key = config.cancelTokenName
      delete config.cancelTokenName

      let sources: any[] = Http.sourceMap.get(key)
      if (!sources) {
        sources = []
        Http.sourceMap.set(key, sources)
      }

      sources = sources.filter(item => item.source)
      Http.sourceMap.set(key, sources)
      sources.forEach(item => {
        if (item.source && item.isPending) {
          item.source.cancel('CancelToken')
          item.isPending = false
        }
      })
      const source: SourceTokenType = {
        source: axios.CancelToken.source(),
        isPending: true,
      }
      sources.push(source)
      config.cancelToken = source.source?.token

      try {
        let result
        if (method === HttpMethod.POST) {
          result = await Http.ajax.post<R>(url, params, config)
        } else if (method === HttpMethod.DEL) {
          result = await Http.ajax.delete<R>(url, config)
        } else if (method === HttpMethod.PUT) {
          result = await Http.ajax.put<R>(url, params, config)
        } else {
          result = await Http.ajax.get<R>(url, config)
        }
        source.source = null
        return result
      } catch (e) {
        source.source = null
        throw e
      }
    } else {
      if (method === HttpMethod.POST) {
        return Http.ajax.post<R>(url, params, config)
      } else if (method === HttpMethod.DEL) {
        return Http.ajax.delete<R>(url, config)
      } else if (method === HttpMethod.PUT) {
        return Http.ajax.put<R>(url, params, config)
      } else {
        return Http.ajax.get<R>(url, config)
      }
    }
  }

  init(): void {
    Http.ajax = axios.create(httpConfig.axios)
    Http.ajax.interceptors.response.use(responseOk, responseErr)
    Http.ajax.interceptors.request.use(cfg => {
      if (appConfig.app.requestIdHeaderName) {
        cfg.headers[appConfig.app.requestIdHeaderName] = 'r' + uuid() + (login.userInfo?.id ?? '');
      }
      if (appConfig.app.traceIdHeaderName) {
        cfg.headers[appConfig.app.traceIdHeaderName] = 't' + uuid() + (login.userInfo?.id ?? '');
      }
      return cfg
    }, (err) => {
      return Promise.reject(err)
    })
  }

  request<R>(config: AxiosRequestConfig): Promise<AxiosResponse<R>> {
    return Http.ajax.request(config)
  }

  get<R>(url: string, params?: Data | Data[] | Array<BaseType | boolean>, config?: HttpRequestConfig): Promise<R> {
    config || (config = {})
    config.headers || (config.headers = {})
    config = moveCancelToken(params, config)
    url += stringify(params, this.questionMark)
    return this.prepare<R>(HttpMethod.GET, url, undefined, config || {})
  }

  del<R>(url: string, params?: Data | Data[] | Array<BaseType | boolean>, config?: HttpRequestConfig): Promise<R> {
    config || (config = {})
    config.headers || (config.headers = {})
    config = moveCancelToken(params, config)
    url += stringify(params, this.questionMark)
    return this.prepare(HttpMethod.DEL, url, undefined, config || {})
  }

  put<R>(url: string, params?: Data | Data[] | Array<BaseType | boolean>, config?: HttpRequestConfig): Promise<R> {
    config || (config = {})
    config.headers || (config.headers = {})
    config = moveCancelToken(params, config)
    removeNullValues(params)
    putChild(config?.headers, CONTENT_TYPE, APPLICATION_JSON)
    return this.prepare(HttpMethod.PUT, url, params, config || {})
  }

  postForm<R>(url: string, params?: Data | Data[] | Array<BaseType | boolean>, config?: HttpRequestConfig): Promise<R> {
    config || (config = {})
    config.headers || (config.headers = {})
    config = moveCancelToken(params, config)
    putChild(config?.headers, CONTENT_TYPE, APPLICATION_FORM_URLENCODED)
    return this.prepare(HttpMethod.POST, url, stringify(params, {
      indices: false,
      allowDots: true,
      skipNulls: true,
    }), config || {})
  }

  post<R>(url: string, params?: Data | Data[] | Array<BaseType | boolean>, config?: HttpRequestConfig): Promise<R> {
    config || (config = {})
    config.headers || (config.headers = {})
    config = moveCancelToken(params, config)
    removeNullValues(params)
    putChild(config?.headers, CONTENT_TYPE, APPLICATION_JSON)
    return this.prepare(HttpMethod.POST, url, params, config || {})
  }

  postFile<R>(url: string, params?: Data | Data[] | Array<BaseType | boolean>, config?: HttpRequestConfig): Promise<R> {
    config || (config = {})
    config.headers || (config.headers = {})
    config = moveCancelToken(params, config)
    putChild(config?.headers, CONTENT_TYPE, MULTIPART_FORM_DATA)
    return this.prepare(HttpMethod.POST, url, params, config || {})
  }

  private async prepare<R>(method: HttpMethod, url: string, params: any, config: HttpRequestConfig): Promise<R> {
    if (!url) {
      throw newError('url 不能为空')
    }
    if (!(url.startsWith(HTTPS_PREFIX) || url.startsWith(HTTP_PREFIX))) {
      setBaseURL(config)
    }
    setTimeout(config)
    httpConfig.setHeaders(config)

    if (httpConfig.isCache && config.cache) {
      const key = config.cache.key || url
      const options = config.cache.options || {}
      const data = getCache(key, null, options)
      if (isNil(data)) {
        const expire = isNil(config.cache.expire) ? 60 * 5 : config.cache.expire
        let result = await httpConfig.beforeRequest<R>(method, url, params, config, Http.request0)
        result = doListToTree(Boolean(config.tree), result)
        result && setCache(key, result, expire, options)
        return result
      } else {
        return data
      }
    } else {
      const tmp = await httpConfig.beforeRequest<R>(method, url, params, config, Http.request0)
      return doListToTree(Boolean(config.tree), tmp)
    }
  }
}
